﻿/*
	Copyright (c) 2013 Andrew Meyer

	Permission is hereby granted, free of charge, to any person obtaining a copy of this software
	and associated documentation files (the "Software"), to deal in the Software without
	restriction, including without limitation the right to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
	Software is furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all copies or
	substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
	BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
	DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using System.Windows.Media.Media3D;
using System.Windows.Threading;

namespace Ascend
{
	/// <summary>
	/// An enumeration of animation states for an Ascend.AnimatedSceneNode or an Ascend.Timeline.
	/// </summary>
	public enum AnimationState
	{
		/// <summary>
		/// The object is not currently playing.
		/// </summary>
		Stopped,

		/// <summary>
		/// The object is currently playing.
		/// </summary>
		Playing,

		/// <summary>
		/// The object is currently playing inside an Ascend.Timeline.
		/// </summary>
		PlayingInTimeline
	}


	/// <summary>
	/// An Ascend.ModelSceneNode that contains animation and armature capabilities. This class
	/// cannot be inherited.
	/// </summary>
	public sealed class AnimatedSceneNode : ModelSceneNode
	{
		#region Lifecycle
		internal AnimatedSceneNode()
			: base()
		{
			// Create the default armature content
			Data.Children.Add(new Model3DGroup());

			AnimationFrames = new ObservableCollection<AnimationFrame>();
			AnimationFrames.CollectionChanged += AnimationFrames_CollectionChanged;

			_animations = new Dictionary<string, FrameInterval>();

			AnimationPoseMask = new AnimationPoseMask();

			_animTimer = new System.Timers.Timer();
		}

		/// <summary>
		/// Creates a new instance of the Ascend.AnimatedSceneNode class.
		/// </summary>
		public static new AnimatedSceneNode Create()
		{
			return new AnimatedSceneNode();
		}

		/// <summary>
		/// Creates a new instance of the Ascend.AnimatedSceneNode class with given model.
		/// </summary>
		/// <param name="model">
		/// The geometric content for the Ascend.AnimatedSceneNode.
		/// </param>
		public static new AnimatedSceneNode Create(GeometryModel3D model)
		{
			return new AnimatedSceneNode
			{
				Model = model
			};
		}
		#endregion

		#region Events
		/// <summary>
		/// Occurs when a non-looping animation completes.
		/// </summary>
		public event SceneNodeAnimationEventHandler AnimationCompleted;
		#endregion

		#region Private Members
		private object _animLock = new object();
		private System.Timers.Timer _animTimer;
		private System.Timers.ElapsedEventHandler _animHandler;
		private DateTime _animStartTime;
		private Dictionary<string, FrameInterval> _animations;

		private Model3DGroup ArmatureVisual
		{
			get { return (Model3DGroup)Data.Children[2]; }
			set { Data.Children[2] = value; }
		}
		#endregion

		#region Public Properties
		/// <summary>
		/// Gets a read-only collection of Ascend.TransformableObject instances that are transformed
		/// relative to this Ascend.AnimatedSceneNode.
		/// </summary>
		public override ReadOnlyCollection<TransformableObject> TransformChildren
		{
			get
			{
				IEnumerable<TransformableObject> transChildren;

				if (Armature != null)
				{
					transChildren = Armature.RootBones.Cast<TransformableObject>().Concat(
						Children.Cast<TransformableObject>());
				}
				else
				{
					transChildren = Children.Cast<TransformableObject>();
				}

				return transChildren.ToList().AsReadOnly();
			}
		}

		/// <summary>
		/// Gets this Ascend.AnimatedSceneNode's list of animation frames.
		/// </summary>
		public ObservableCollection<AnimationFrame> AnimationFrames
		{
			get;
			private set;
		}

		private int _currentFrame = -1;
		/// <summary>
		/// Gets or sets this Ascend.AnimatedSceneNode's current animation frame.
		/// </summary>
		/// <remarks>
		/// The default value for this property is -1, specifying an invalid frame. CurrentFrame
		/// will have this value when the Ascend.AnimatedSceneNode is initialized and whenever the
		/// Ascend.AnimatedSceneNode's AnimationFrames list is modified.
		/// </remarks>
		public int CurrentFrame
		{
			get
			{
				return _currentFrame;
			}

			set
			{
				if (_currentFrame == value)
				{
					return;
				}

				if (value < 0 || value >= AnimationFrames.Count())
				{
					throw new ArgumentOutOfRangeException("value", value,
						"Value must be greater than or equal to zero and less than the size of " +
						"AnimationFrames");
				}

				_currentFrame = value;

				AnimationFrame frame = AnimationFrames[_currentFrame];

				if (Armature != null && frame.BonePoses.Any())
				{
					Armature.ApplyBonePoses(frame.BonePoses);
				}

				if (frame.ModelPose != null)
				{
					ApplyPose(frame.ModelPose);
				}
			}
		}

		/// <summary>
		/// Get this Ascend.AnimatedSceneNode's current animation state.
		/// </summary>
		public AnimationState AnimationState
		{
			get;
			internal set;
		}

		/// <summary>
		/// Gets the name of the currently playing animation, or null if no animation is playing.
		/// </summary>
		public string CurrentAnimation
		{
			get;
			internal set;
		}

		/// <summary>
		/// Gets or sets the mask that determines which transform properties should be updated
		/// during animation.
		/// </summary>
		public AnimationPoseMask AnimationPoseMask
		{
			get;
			set;
		}

		private Armature _armature;
		/// <summary>
		/// Gets or sets this Ascend.AnimatedSceneNode's armature.
		/// </summary>
		public Armature Armature
		{
			get
			{
				return _armature;
			}

			set
			{
				if (_armature == value)
				{
					return;
				}

				if (value == null)
				{
					RemoveArmature();
				}
				else
				{
					SetArmature(value);
				}
			}
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Play this Ascend.AnimatedSceneNode's specified animation.
		/// </summary>
		/// <param name="animationName">
		/// The name of the animation frame interval to play.
		/// </param>
		/// <param name="loop">
		/// Whether or not to loop the animation.
		/// </param>
		/// <param name="restart">
		/// Whether or not to start the animation from the beginning.
		/// </param>
		/// <param name="frameRate">
		/// Animation speed in frames per second. Specifying a negative value will play the
		/// animation in reverse.
		/// </param>
		/// <exception cref="System.ArgumentException">
		/// Thrown if <paramref name="animationName"/> is not a defined animation on this
		/// AnimatedSceneNode -or- if <paramref name="frameRate"/> is zero.
		/// </exception>
		/// <exception cref="System.InvalidOperationException">
		/// Thrown if this Ascend.AnimatedSceneNode is currently being animated in an
		/// Ascend.Timeline.
		/// </exception>
		public void Play(string animationName, bool loop = false, bool restart = false,
			double frameRate = 60.0)
		{
			if (!_animations.ContainsKey(animationName))
			{
				throw new ArgumentException(string.Format(
					"Animation '{0}' does not exist on AnimatedSceneNode.", animationName),
					"animationName");
			}

			if (AnimationState == AnimationState.PlayingInTimeline)
			{
				throw new InvalidOperationException(
					"Cannot play AnimatedSceneNode while it is being animated in a Timeline.");
			}

			if (AnimationState == AnimationState.Playing)
			{
				Pause();
			}

			if (frameRate == 0.0)
			{
				throw new ArgumentException("Frame rate must be a non-zero value.", "frameRate");
			}

			FrameInterval interval = _animations[animationName];
			bool reverse = frameRate < 0.0;

			StartAnimation(animationName, loop, restart, reverse, frameRate, interval);
		}

		/// <summary>
		/// Pauses any currently running animation on this Ascend.AnimatedSceneNode.
		/// </summary>
		/// <exception cref="System.InvalidOperationException">
		/// Thrown if this Ascend.AnimatedSceneNode is currently being animated inside an
		/// Ascend.Timeline.
		/// </exception>
		public void Pause()
		{
			if (AnimationState == AnimationState.PlayingInTimeline)
			{
				throw new InvalidOperationException(
					"Cannot pause AnimatedSceneNode while it is being animated in a Timeline.");
			}

			PauseInternal();
		}

		/// <summary>
		/// Defines an animation frame interval on this Ascend.AnimatedSceneNode. 
		/// </summary>
		/// <param name="animationName">
		/// The name of the animation.</param>
		/// <param name="frameInterval">
		/// The frame interval that defines the animation.
		/// </param>
		/// <remarks>
		/// If a definition already exists by the given name, it will be overwritten.
		/// </remarks>
		/// <exception cref="System.ArgumentOutOfRangeException">
		/// Thrown if <paramref name="frameInterval"/> is outside the range of AnimationFrames.
		/// </exception>
		public void DefineAnimation(string animationName, FrameInterval frameInterval)
		{
			if (frameInterval.Start < 0 || frameInterval.End >= AnimationFrames.Count)
			{
				throw new ArgumentOutOfRangeException("frameInterval", frameInterval,
					"Interval outside range of AnimationFrames");
			}

			_animations[animationName] = frameInterval;
		}

		/// <summary>
		/// Defines a named animation frame interval on this Ascend.AnimatedSceneNode. 
		/// </summary>
		/// <param name="animationName">
		/// The name of the animation.
		/// </param>
		/// <param name="start">
		/// The starting animation frame index.
		/// </param>
		/// <param name="end">
		/// The ending animation frame index.
		/// </param>
		/// <remarks>
		/// If a definition already exists by the given name, it will be overwritten.
		/// </remarks>
		/// <exception cref="System.ArgumentOutOfRangeException">
		/// Thrown if <paramref name="start"/> is greater than <paramref name="end"/> -or- if
		/// <paramref name="start"/> and <paramref name="end"/> define an interval that is outside
		/// the range of AnimationFrames.</exception>
		public void DefineAnimation(string animationName, int start, int end)
		{
			DefineAnimation(animationName, new FrameInterval(start, end));
		}

		/// <summary>
		/// Removes the specified animation from this Ascend.AnimatedSceneNode.
		/// </summary>
		/// <param name="animationName">
		/// The name of the animation to remove.
		/// </param>
		public void RemoveAnimation(string animationName)
		{
			_animations.Remove(animationName);
		}

		/// <summary>
		/// Gets the specified animation's frame interval.
		/// </summary>
		/// <param name="animationName">
		/// The name of the animation.
		/// </param>
		/// <returns>
		/// Returns an Ascend.FrameInterval specifying the start and end frame for the animation.
		/// </returns>
        /// <exception cref="System.ArgumentException">
		/// Thrown if <paramref name="animationName"/> is not defined on this AnimatedSceneNode.
		/// </exception>
		public FrameInterval GetAnimation(string animationName)
		{
			if (!_animations.ContainsKey(animationName))
			{
				throw new ArgumentException(string.Format(
					"Animation '{0}' does not exist on AnimatedSceneNode.", animationName),
					"animationName");
			}

			return _animations[animationName];
		}

		/// <summary>
		/// Lists the animations defined on this Ascend.AnimatedSceneNode.
		/// </summary>
		/// <returns>
		/// Returns a read-only collection containing the names of all animations defined on this
		/// Ascend.AnimatedSceneNode.
		/// </returns>
		public ReadOnlyCollection<string> ListAnimations()
		{
			return _animations.Keys.ToList().AsReadOnly();
		}

		/// <summary>
		/// Creates a deep copy of this Ascend.AnimatedSceneNode, including a deep copy of its
		/// Ascend.Armature and deep copies of all children.
		/// </summary>
		/// <returns>
		/// Returns the deep copy.
		/// </returns>
		public override TransformableObject Clone()
		{
			AnimatedSceneNode clone = new AnimatedSceneNode();

			CloneProperties(clone);
			CloneChildren(clone);

			return clone;
		}
		#endregion

		#region Internal Methods
		internal void PauseInternal()
		{
			if (_animHandler != null)
			{
				_animTimer.Stop();
				_animTimer.Elapsed -= _animHandler;
				_animHandler = null;

				AnimationState = AnimationState.Stopped;
				CurrentAnimation = null;
			}
		}
		#endregion

		#region Protected Methods
		protected override void CloneProperties(SceneNode clone)
		{
			base.CloneProperties(clone);

			AnimatedSceneNode animClone = (AnimatedSceneNode)clone;

			// Clone armature
			animClone.Armature = (Armature != null)
				? Armature.Clone()
				: null;

			// Clone animation frames
			CloneAnimationFrames(animClone);
			animClone.CurrentFrame = CurrentFrame;

			// Clone animation definitions
			foreach (var kvp in _animations)
			{
				animClone.DefineAnimation(kvp.Key, kvp.Value);
			}

			// Clone pose mask
			animClone.AnimationPoseMask = (AnimationPoseMask != null)
				? AnimationPoseMask.Clone()
				: null;
		}
		#endregion

		#region Private Methods
		private void SetArmature(Armature armature)
		{
			// Clear old skin
			armature.ClearSkin();

			// Set armature data
			ArmatureVisual = armature.Data;

			// Update armature root bone transform parents
			foreach (Bone bone in armature.RootBones)
			{
				bone._transformParent = this;
			}

			// Set new skin
			armature._skin = this;

			// Set armature
			_armature = armature;
		}

		internal void RemoveArmature()
		{
			// Remove armature data by replacing with default
			ArmatureVisual = new Model3DGroup();

			// Clear armature's skin
			_armature._skin = null;

			// Update armature root bone transform parents
			foreach (Bone bone in _armature.RootBones)
			{
				bone._transformParent = null;
			}

			// Clear armature
			_armature = null;
		}

		private void ApplyPose(ObjectPose pose)
		{
			Dispatcher.BeginInvoke((Action)(() =>
				{
					if (AnimationPoseMask == null || AnimationPoseMask.X)
					{
						X = pose.X;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.Y)
					{
						Y = pose.Y;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.Z)
					{
						Z = pose.Z;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.AngleX)
					{
						AngleX = pose.AngleX;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.AngleY)
					{
						AngleY = pose.AngleY;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.AngleZ)
					{
						AngleZ = pose.AngleZ;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.ScaleX)
					{
						ScaleX = pose.ScaleX;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.ScaleY)
					{
						ScaleY = pose.ScaleY;
					}

					if (AnimationPoseMask == null || AnimationPoseMask.ScaleZ)
					{
						ScaleZ = pose.ScaleZ;
					}
				}));
		}

		private void StartAnimation(string animationName, bool loop, bool restart,
			bool reverse, double frameRate, FrameInterval interval)
		{
			int refFrame = GetReferenceFrame(restart, reverse, interval);

			AnimationState = AnimationState.Playing;
			CurrentAnimation = animationName;
			
			_animStartTime = DateTime.Now;
			_animTimer.Interval = 1000 / Math.Abs(frameRate);
			
			_animHandler = (sender, e) =>
				{
					if (Monitor.TryEnter(_animLock))
					{
						try
						{
							double frameDelta = Math.Abs(frameRate) *
								(DateTime.Now - _animStartTime).TotalSeconds;

							if (!ProgressAnimation(refFrame, frameDelta, loop, reverse,
								interval))
							{
								Pause();
								RaiseAnimationCompletedEvent(animationName);
							}
						}
						finally
						{
							Monitor.Exit(_animLock);
						}
					}
				};

			_animTimer.Elapsed += _animHandler;
			_animTimer.Start();
		}

		private int GetReferenceFrame(bool restart, bool reverse, FrameInterval interval)
		{
			int frame = CurrentFrame;

			if (restart || frame < interval.Start || frame > interval.End)
			{
				frame = reverse
					? interval.End
					: interval.Start;
			}

			return frame;
		}

		// Progresses animation and returns whether or not the animation should continue.
		private bool ProgressAnimation(int refFrame, double frameDelta, bool loop, bool reverse,
			FrameInterval interval)
		{
			// Get next frame (keep as double to retain values greater than Integer.MaxValue or less
			// than Integer.MinValue)
			double nextFrame = reverse
				? refFrame - frameDelta
				: refFrame + frameDelta;

			// Check for wrap-around
			if ((nextFrame <= interval.Start && reverse) || (nextFrame >= interval.End && !reverse))
			{
				if (!loop)
				{
					// Clamp to end of animation
					CurrentFrame = reverse
						? interval.Start
						: interval.End;

					return false;
				}

				// Compute wrapped-around frame number
				nextFrame =
					Utilities.RealMod(nextFrame - interval.Start, interval.Length) + interval.Start;
			}

			CurrentFrame = (int)nextFrame;

			return true;
		}

		private void CloneAnimationFrames(AnimatedSceneNode clone)
		{
			List<Bone> origBones = (Armature != null)
				? Armature.ListBones()
				: new List<Bone>();

			List<Bone> clonedBones = (clone.Armature != null)
				? clone.Armature.ListBones()
				: new List<Bone>();

			foreach (AnimationFrame origFrame in AnimationFrames)
			{
				AnimationFrame clonedFrame = new AnimationFrame();

				clonedFrame.ModelPose = (origFrame.ModelPose != null)
					? origFrame.ModelPose.Clone()
					: null;
				
				for (int b = 0; b < clonedBones.Count; b++)
				{
					Bone origBone = origBones[b];
					Bone clonedBone = clonedBones[b];

					if (origFrame.BonePoses.ContainsKey(origBone))
					{
						clonedFrame.BonePoses.Add(clonedBone,
							origFrame.BonePoses[origBone].Clone());
					}
				}

				clone.AnimationFrames.Add(clonedFrame);
			}
		}
		#endregion

		#region Event Invokers
		#region AnimationCompleted
		private void RaiseAnimationCompletedEvent(string animationName)
		{
			if (AnimationCompleted != null)
			{
				SceneNodeAnimationEventArgs e = new SceneNodeAnimationEventArgs
				{
					AnimationName = animationName
				};

				foreach (Delegate del in AnimationCompleted.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion
		#endregion

		#region Event Handlers
		private void AnimationFrames_CollectionChanged(object sender,
			NotifyCollectionChangedEventArgs e)
		{
			// Invalidate current frame
			_currentFrame = -1;
		}
		#endregion
	}
}
